spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import { ImageResponse } from 'next/og';2import { api, safe } from '@/lib/api';3import { fmtDate, fmtInt } from '@/lib/format';4import { OBJECT_TYPE_LABELS, ORBIT_CLASS_COLORS, STATUS_COLORS } from '@/lib/site';56export const size = { width: 1200, height: 630 };7export const contentType = 'image/png';8export const alt = 'SatelliteIndex — satellite orbit card';910// OG renderer cannot resolve CSS variables: mirror the hex tokens from globals.css.11const HEX: Record<string, string> = {12 'var(--active)': '#38d17f',13 'var(--inactive)': '#7c869e',14 'var(--ink-3)': '#6b7694',15 'var(--warn)': '#f5a524',16 'var(--danger)': '#ff5c6c',17 'var(--accent-2)': '#8f7dff',18 'var(--leo)': '#38d3ff',19 'var(--meo)': '#8f7dff',20 'var(--geo)': '#f5b544',21 'var(--heo)': '#ff7ab6',22 'var(--other)': '#7c869e',23};24const hex = (v: string | undefined, fallback: string) => (v ? HEX[v] ?? fallback : fallback);2526export default async function Image({ params }: { params: Promise<{ slug: string }> }) {27 const { slug } = await params;28 const res = await safe(api.satellite(slug));29 const d = res?.data ?? null;30 const statusColor = hex(STATUS_COLORS[d?.status ?? 'UNKNOWN'], '#7c869e');31 const orbitColor = hex(ORBIT_CLASS_COLORS[d?.orbit_class ?? 'UNKNOWN'], '#7c869e');32 const live = d?.live && d.live.error == null ? d.live : null;33 const alt = live ? `${fmtInt(live.altitude_km)} km` : d?.orbital_state ? `${fmtInt(d.orbital_state.perigee_km)}–${fmtInt(d.orbital_state.apogee_km)} km` : '—';3435 return new ImageResponse(36 (37 <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: 64, background: 'linear-gradient(135deg, #0b1020 0%, #060912 70%)', color: '#eaf0ff', fontFamily: 'sans-serif' }}>38 <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>39 <div style={{ width: 18, height: 18, borderRadius: 999, background: '#38d3ff', boxShadow: '0 0 0 6px rgba(56,211,255,0.18)' }} />40 <div style={{ fontSize: 26, letterSpacing: 4, textTransform: 'uppercase', color: '#a3aec8', fontWeight: 600 }}>SatelliteIndex</div>41 {d && (42 <div style={{ marginLeft: 'auto', fontSize: 24, color: '#6b7694', letterSpacing: 2 }}>{`NORAD ${d.norad_id ?? '—'}${d.cospar_id ? ` · ${d.cospar_id}` : ''}`}</div>43 )}44 </div>4546 {d ? (47 <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>48 <div style={{ fontSize: d.name.length > 22 ? 64 : 88, fontWeight: 700, letterSpacing: -3, lineHeight: 1 }}>{d.name}</div>49 <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>50 <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 18px', borderRadius: 999, border: `2px solid ${statusColor}`, color: statusColor, fontSize: 26 }}>51 <div style={{ width: 12, height: 12, borderRadius: 999, background: statusColor }} />52 {d.status.charAt(0) + d.status.slice(1).toLowerCase()}53 </div>54 <div style={{ padding: '8px 18px', borderRadius: 8, background: 'rgba(160,180,230,0.10)', color: '#a3aec8', fontSize: 26 }}>{OBJECT_TYPE_LABELS[d.object_type] ?? d.object_type}</div>55 {d.orbit_class && <div style={{ padding: '8px 18px', borderRadius: 8, background: 'rgba(160,180,230,0.10)', color: orbitColor, fontSize: 26, fontWeight: 700 }}>{d.orbit_class}</div>}56 {d.operator_name && <div style={{ fontSize: 26, color: '#a3aec8' }}>{`· ${d.operator_name}`}</div>}57 </div>58 </div>59 ) : (60 <div style={{ fontSize: 72, fontWeight: 700 }}>Object not catalogued</div>61 )}6263 <div style={{ display: 'flex', gap: 56, borderTop: '1px solid rgba(160,180,230,0.2)', paddingTop: 28 }}>64 {[65 ['Altitude', alt],66 ['Inclination', d?.orbital_state ? `${d.orbital_state.inclination.toFixed(2)}°` : d?.inclination_deg != null ? `${Number(d.inclination_deg).toFixed(2)}°` : '—'],67 ['Period', d?.orbital_state?.period_minutes ? `${d.orbital_state.period_minutes.toFixed(1)} min` : '—'],68 ['Launched', fmtDate(d?.launch_date)],69 ].map(([k, v]) => (70 <div key={k} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>71 <div style={{ fontSize: 20, letterSpacing: 3, textTransform: 'uppercase', color: '#6b7694' }}>{k}</div>72 <div style={{ fontSize: 40, fontWeight: 600, color: '#eaf0ff' }}>{v}</div>73 </div>74 ))}75 </div>76 </div>77 ),78 { ...size },79 );80}81